Skip to content

fix: transactions paging using effective order #972

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

gfyrag
Copy link
Contributor

@gfyrag gfyrag commented Jun 16, 2025

No description provided.

@gfyrag gfyrag requested a review from a team as a code owner June 16, 2025 10:08
Copy link

coderabbitai bot commented Jun 16, 2025

Walkthrough

The changes introduce a mechanism in the ledger's paginator to convert pagination IDs to the correct SQL types based on table schema metadata. This is achieved by adding a Table field to the paginator and using it to map pagination columns to their SQL types. The tests are updated to verify correct pagination behavior with effective ordering.

Changes

File(s) Change Summary
internal/storage/ledger/paginator_column.go Added Table field to columnPaginator; introduced convertPaginationIDToSQLType for SQL type conversion; updated pagination logic to use converted IDs.
internal/storage/ledger/store.go Set the Table field in columnPaginator for Transactions() and Logs() to associate with the correct table metadata.
internal/storage/driver/module.go Registered ledger database models (Transaction, Log, Account, Move, Ledger) with Bun ORM dialect during FX module initialization.
test/e2e/api_transactions_list_test.go Added tests for paginated requests using the next-page cursor with effective ordering, asserting correct data length.
internal/storage/ledger/resource.go Removed commented-out debug print statement in paginated resource repository.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant API
    participant Store
    participant Paginator
    participant Schema

    Client->>API: Request transactions with pagination
    API->>Store: Fetch transactions (with paginator)
    Store->>Paginator: Initialize with Table metadata
    Paginator->>Schema: Lookup column SQL type
    Paginator->>Paginator: Convert PaginationID to SQL type
    Paginator->>Store: Return paginated query
    Store->>API: Return transactions
    API->>Client: Respond with paginated result
Loading

Possibly related PRs

  • fix: pagination #936: Both PRs modify the paginator logic in columnPaginator, with this PR handling SQL type conversion for pagination IDs and the related PR enabling pagination column overrides.

Suggested reviewers

  • Dav-14

Poem

In the fields of code where ledgers grow,
A rabbit hops where paginators flow.
IDs now match their SQL kind,
No mismatched types left behind!
With tests that prove the way is right,
Paginated pages hop into sight.
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate Unit Tests
  • Create PR with Unit Tests
  • Commit Unit Tests in branch hotfix/v2.2/transactions-paging-using-effective-order
  • Post Copyable Unit Tests in Comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai auto-generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
internal/storage/ledger/paginator_column.go (1)

73-81: Handle all timestamp variants in convertPaginationIDToSQLType

PostgreSQL reports timestamp columns as "timestamp without time zone" and "timestamp with time zone" (AKA timestamptz).
Currently only the first variant is handled, so paginating on a timestamptz column will silently fall back to *big.Int, leading to mismatched types in the SQL driver.

-case "timestamp without time zone", "timestamp":
+case "timestamp without time zone", "timestamp", "timestamp with time zone", "timestamptz":

(Optional) cover date, time, and numeric types explicitly for completeness.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eed0acc and 078e0ba.

📒 Files selected for processing (3)
  • internal/storage/ledger/paginator_column.go (4 hunks)
  • internal/storage/ledger/store.go (2 hunks)
  • test/e2e/api_transactions_list_test.go (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
test/e2e/api_transactions_list_test.go (2)
pkg/testserver/api.go (1)
  • ListTransactions (139-147)
pkg/client/models/operations/v2listtransactions.go (1)
  • V2ListTransactionsRequest (36-53)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Dirty
  • GitHub Check: Tests
🔇 Additional comments (1)
test/e2e/api_transactions_list_test.go (1)

222-237: Good addition – validates cursor paging with effective ordering

The new “using next page” context exercises the exact scenario that broke previously.
It keeps the request minimal (only cursor), ensuring the server picks the page size from the encoded cursor, and the HaveLen(pageSize) assertion makes that explicit.
Nice coverage.

Comment on lines 65 to 70
return newPaginatedResourceRepository(store, store.ledger, &transactionsResourceHandler{}, columnPaginator[ledger.Transaction, any]{
defaultPaginationColumn: "id",
defaultOrder: bunpaginate.OrderDesc,
Table: store.db.Dialect().Tables().ByName("transactions"),
})
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Guard against nil table metadata to avoid panics at runtime

store.db.Dialect().Tables().ByName("transactions") (and "logs") can legally return nil if the schema introspection misses the table name (e.g. because of a non-default schema, quoting, or a typo).
columnPaginator.paginate() immediately dereferences o.Table.FieldMap[...], which would panic in that situation (or when a caller specifies a Column that is not present in the FieldMap).

Consider resolving the table upfront and failing fast (or falling back) instead of letting the paginator crash later:

- Table: store.db.Dialect().Tables().ByName("transactions"),
+ tbl := store.db.Dialect().Tables().ByName("transactions")
+ if tbl == nil {
+     // You may want to log and bubble up a coherent error instead of panicking
+     panic("transactions table metadata not found – paginator needs schema information")
+ }
+ Table: tbl,

Do the same for the "logs" paginator or, even better, extend columnPaginator.paginate() to tolerate a missing Table / column and fall back to the raw big.Int value.
Fail-fast here prevents production panics and makes the error surface early in the boot path.

Also applies to: 76-81

🤖 Prompt for AI Agents
In internal/storage/ledger/store.go around lines 65 to 70 and 76 to 81, the
calls to store.db.Dialect().Tables().ByName("transactions") and ByName("logs")
can return nil if the table metadata is missing, which causes panics later when
the paginator dereferences the table fields. To fix this, add checks immediately
after retrieving the table metadata to verify it is not nil. If nil, return an
error or handle the fallback gracefully to fail fast during initialization
instead of panicking at runtime. This ensures safer handling of missing table
metadata and prevents crashes.

Comment on lines 17 to 20
defaultPaginationColumn string
defaultOrder bunpaginate.Order
Table *schema.Table
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Table should be nullable-safe or mandatory – pick one

You added the Table *schema.Table handle but client code can still instantiate columnPaginator{} without setting it (e.g., in future resources).
Because paginate() dereferences o.Table unconditionally, any forgotten assignment will panic.

Either:

  1. Make the field non-pointer (schema.Table) and always populate it, or
  2. Keep it a pointer but add defensive checks in paginate() (see next comment).
🤖 Prompt for AI Agents
In internal/storage/ledger/paginator_column.go around lines 17 to 20, the Table
field is a pointer but can be nil, causing a panic when dereferenced in
paginate(). To fix this, either change Table to a non-pointer type and ensure it
is always set when creating columnPaginator instances, or keep it as a pointer
but add nil checks in paginate() to handle the case when Table is not assigned,
preventing runtime panics.

Comment on lines +49 to 66
paginationID := convertPaginationIDToSQLType(
o.Table.FieldMap[paginationColumn].DiscoveredSQLType,
query.PaginationID,
)
if query.Reverse {
switch originalOrder {
case bunpaginate.OrderAsc:
sb = sb.Where(fmt.Sprintf("%s < ?", paginationColumn), query.PaginationID)
sb = sb.Where(fmt.Sprintf("%s < ?", paginationColumn), paginationID)
case bunpaginate.OrderDesc:
sb = sb.Where(fmt.Sprintf("%s > ?", paginationColumn), query.PaginationID)
sb = sb.Where(fmt.Sprintf("%s > ?", paginationColumn), paginationID)
}
} else {
switch originalOrder {
case bunpaginate.OrderAsc:
sb = sb.Where(fmt.Sprintf("%s >= ?", paginationColumn), query.PaginationID)
sb = sb.Where(fmt.Sprintf("%s >= ?", paginationColumn), paginationID)
case bunpaginate.OrderDesc:
sb = sb.Where(fmt.Sprintf("%s <= ?", paginationColumn), query.PaginationID)
sb = sb.Where(fmt.Sprintf("%s <= ?", paginationColumn), paginationID)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Dereferencing FieldMap without checks is unsafe

o.Table.FieldMap[paginationColumn] will panic when:

  • o.Table is nil.
  • paginationColumn is not present in the table (e.g., user passed a computed column such as effective).

A minimal, crash-free fallback:

- paginationID := convertPaginationIDToSQLType(
-     o.Table.FieldMap[paginationColumn].DiscoveredSQLType,
-     query.PaginationID,
- )
+ var sqlType string
+ if o.Table != nil {
+     if field, ok := o.Table.FieldMap[paginationColumn]; ok {
+         sqlType = field.DiscoveredSQLType
+     }
+ }
+ paginationID := convertPaginationIDToSQLType(sqlType, query.PaginationID)

This still honours type conversion when metadata exists but gracefully degrades to the previous behaviour otherwise.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
paginationID := convertPaginationIDToSQLType(
o.Table.FieldMap[paginationColumn].DiscoveredSQLType,
query.PaginationID,
)
if query.Reverse {
switch originalOrder {
case bunpaginate.OrderAsc:
sb = sb.Where(fmt.Sprintf("%s < ?", paginationColumn), query.PaginationID)
sb = sb.Where(fmt.Sprintf("%s < ?", paginationColumn), paginationID)
case bunpaginate.OrderDesc:
sb = sb.Where(fmt.Sprintf("%s > ?", paginationColumn), query.PaginationID)
sb = sb.Where(fmt.Sprintf("%s > ?", paginationColumn), paginationID)
}
} else {
switch originalOrder {
case bunpaginate.OrderAsc:
sb = sb.Where(fmt.Sprintf("%s >= ?", paginationColumn), query.PaginationID)
sb = sb.Where(fmt.Sprintf("%s >= ?", paginationColumn), paginationID)
case bunpaginate.OrderDesc:
sb = sb.Where(fmt.Sprintf("%s <= ?", paginationColumn), query.PaginationID)
sb = sb.Where(fmt.Sprintf("%s <= ?", paginationColumn), paginationID)
}
// Safely derive the SQL type for pagination, falling back if metadata is missing.
var sqlType string
if o.Table != nil {
if field, ok := o.Table.FieldMap[paginationColumn]; ok {
sqlType = field.DiscoveredSQLType
}
}
paginationID := convertPaginationIDToSQLType(sqlType, query.PaginationID)
if query.Reverse {
switch originalOrder {
case bunpaginate.OrderAsc:
sb = sb.Where(fmt.Sprintf("%s < ?", paginationColumn), paginationID)
case bunpaginate.OrderDesc:
sb = sb.Where(fmt.Sprintf("%s > ?", paginationColumn), paginationID)
}
} else {
switch originalOrder {
case bunpaginate.OrderAsc:
sb = sb.Where(fmt.Sprintf("%s >= ?", paginationColumn), paginationID)
case bunpaginate.OrderDesc:
sb = sb.Where(fmt.Sprintf("%s <= ?", paginationColumn), paginationID)
}
}
🤖 Prompt for AI Agents
In internal/storage/ledger/paginator_column.go around lines 49 to 66, the code
dereferences o.Table.FieldMap[paginationColumn] without checking if o.Table is
nil or if paginationColumn exists in FieldMap, which can cause a panic. Fix this
by adding checks to ensure o.Table is not nil and paginationColumn is present in
FieldMap before accessing it. If these conditions are not met, fall back to the
previous behavior without type conversion to avoid crashes while still honoring
type conversion when metadata exists.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
internal/storage/driver/module.go (2)

5-5: Import alias ledger is easily confused with ledgerstore

You now have two imports whose aliases differ only by the suffix store. Although legal, this can slow down review/reading because the two names are visually similar but point to very different packages. Consider a more explicit alias (e.g. coreledger or domain) for one of them to reduce cognitive load.


25-33: Register models via db.RegisterModel and use nil-pointers to avoid needless allocations

db.Dialect().Tables().Register(&T{}) works, but:

  1. bun.DB exposes RegisterModel(...) which is shorter and future-proof across dialect implementations.
  2. Passing an address-of value allocates five struct instances every time the Fx graph is built. Using typed nil pointers avoids that cost while retaining type information ((*ledger.Transaction)(nil)).

Example diff:

-		fx.Invoke(func(db *bun.DB) {
-			db.Dialect().Tables().Register(
-				&ledger.Transaction{},
-				&ledger.Log{},
-				&ledger.Account{},
-				&ledger.Move{},
-				&ledger.Ledger{},
-			)
-		}),
+		fx.Invoke(func(db *bun.DB) {
+			db.RegisterModel(
+				(*ledger.Transaction)(nil),
+				(*ledger.Log)(nil),
+				(*ledger.Account)(nil),
+				(*ledger.Move)(nil),
+				(*ledger.Ledger)(nil),
+			)
+		}),

This keeps the intent intact, shaves a tiny allocation, and leans on the higher-level helper Bun already provides.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 078e0ba and db9f2e7.

📒 Files selected for processing (2)
  • internal/storage/driver/module.go (2 hunks)
  • internal/storage/ledger/resource.go (0 hunks)
💤 Files with no reviewable changes (1)
  • internal/storage/ledger/resource.go
🧰 Additional context used
🧬 Code Graph Analysis (1)
internal/storage/driver/module.go (3)
internal/storage/ledger/logs.go (1)
  • Log (20-26)
internal/account.go (1)
  • Account (14-24)
internal/moves.go (1)
  • Move (12-24)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Tests

@gfyrag gfyrag merged commit 9e5c530 into release/v2.2 Jun 16, 2025
8 checks passed
@gfyrag gfyrag deleted the hotfix/v2.2/transactions-paging-using-effective-order branch June 16, 2025 12:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants